关于 phaser-audio
phaser-audio 是面向 Phaser 4 的浏览器游戏音频技能,介绍音频后端选择,以及资源加载、播放和生命周期管理等实用模式。
主要功能
- 01Web Audio 与 HTML5 Audio 指引
- 02MP3 和 OGG 资源加载
- 03音乐与单次音效模式
- 04声音池与音频精灵
- 05音量、解锁与恢复处理
- 06场景清理和音乐过渡
适用场景
- 01选择音频后端和兼容格式
- 02添加循环音乐与单次音效
- 03处理快速重复触发的音效
- 04在标签页挂起后恢复音频
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ yakoub-ai/ phaser4-gamedev --skill phaser-audio安装后在 Claude Code 中输入/phaser-audio确认已加载。
附带文件references/audio-api.md
详细介绍
Phaser 4 音频
Phaser 4 使用 Web Audio API 处理所有音频播放。了解各音频后端的适用场景,并遵循以下加载和播放模式,避免最常见的音频问题。
Web Audio 与 HTML5 Audio
所有支持 Web Audio 的浏览器(所有现代浏览器)中,Phaser 都默认使用 Web Audio。Web Audio 在专用线程上处理音频,支持空间音频、效果链和精确调度,而且不会受到困扰 HTML5 Audio 的单轨限制。
当 AudioContext 不可用时,Phaser 会自动回退到 HTML5 Audio——通常是较旧的 Android WebView 和某些边缘浏览器配置。你很少会刻意使用它。如果必须强制使用:
const config: Phaser.Types.Core.GameConfig = {
audio: {
disableWebAudio: true, // force HTML5 Audio fallback
},
};
除非有特定的兼容性要求,否则绝不要强制使用 HTML5 Audio。 它的限制——只能同时播放一条音轨、不支持音调偏移或速率调整,也不支持空间定位——会限制你的设计。
加载音频:始终同时提供 mp3 和 ogg
浏览器对音频编解码器的支持并不一致。Safari 和 iOS 需要 mp3。Firefox 更偏好 ogg/vorbis。Chrome 两种格式都支持。请同时提供这两种格式,让 Phaser 选择浏览器支持的格式:
// preload()
preload(): void {
// ALWAYS provide both formats as an array — mp3 first, ogg second
this.load.audio('music-main', ['assets/audio/main.mp3', 'assets/audio/main.ogg']);
this.load.audio('sfx-jump', ['assets/audio/jump.mp3', 'assets/audio/jump.ogg']);
this.load.audio('sfx-coin', ['assets/audio/coin.mp3', 'assets/audio/coin.ogg']);
this.load.audio('sfx-hurt', ['assets/audio/hurt.mp3', 'assets/audio/hurt.ogg']);
}
绝不要只加载一种格式。如果只提供 .ogg,那么在 Chrome 开发机上运行完美的游戏,在 Safari/iOS 上将完全没有声音。
添加和播放声音
this.sound.add()——完全控制
如果需要引用声音以控制它(暂停、恢复、跳转、调整音量、监听事件),请使用 add():
create(): void {
const music = this.sound.add('music-main', {
loop: true,
volume: 0.6,
});
music.play();
this.music = music;
}
this.sound.play()——一次性即播即弃
对于只播放一次、之后无需再引用的声音,请使用管理器的 play()。Phaser 会自动管理其生命周期:
// In update() or an event handler:
this.sound.play('sfx-coin', { volume: 0.9 });
this.sound.play('sfx-jump', { volume: 1.0, rate: 1.1 });
经验法则: 背景音乐 → add()。一次性音效 → sound.play()。
SoundConfig 选项
将 SoundConfig 对象作为 add() 的第二个参数,或作为 play() 的第二个参数传入:
const config: Phaser.Types.Sound.SoundConfig = {
loop: false, // boolean — loop the sound (default: false)
volume: 1, // number 0–1 — per-sound volume (default: 1)
rate: 1, // number — playback speed multiplier (default: 1; 2 = double speed)
detune: 0, // number — cents offset from base pitch (default: 0; 100 = 1 semitone)
seek: 0, // number — start position in seconds (default: 0)
delay: 0, // number — seconds to wait before playing (default: 0)
};
背景音乐
背景音乐会循环播放,需要音量控制,并且必须在场景关闭时停止。
// --- In PreloaderScene.preload() ---
this.load.audio('music-game', ['assets/audio/game.mp3', 'assets/audio/game.ogg']);
this.load.audio('music-menu', ['assets/audio/menu.mp3', 'assets/audio/menu.ogg']);
// --- In GameScene.create() ---
this.bgMusic = this.sound.add('music-game', { loop: true, volume: 0.5 });
this.bgMusic.play();
// Adjust volume at runtime
this.bgMusic.setVolume(0.3);
// Pause/resume (e.g. on pause screen)
this.bgMusic.pause();
this.bgMusic.resume();
// Stop (e.g. on scene shutdown)
this.bgMusic.stop();
音效模式:一次性声音
对于播放后即弃的音效——拾取硬币、枪声、UI 点击声——请直接使用 this.sound.play()。不要存储引用:
// In any scene method or event handler:
this.sound.play('sfx-coin', { volume: 0.9 });
this.sound.play('sfx-jump', { volume: 1.0, rate: 1.05 });
this.sound.play('sfx-death', { volume: 0.8, detune: -200 });
Phaser 会创建一个内部实例,播放至结束后再销毁。无需进行任何清理。
快速连续播放音效时的声音池
如果某个声音每秒播放多次(枪声、脚步声、快速的 UI 反馈),单个实例会导致声音被截断——每次调用新的 play() 都会让同一个声音从头开始播放。
预先创建一组实例,并按轮转顺序使用:
create(): void {
// Create a pool of 5 gunshot sounds
this.gunshotPool = [];
for (let i = 0; i < 5; i++) {
this.gunshotPool.push(this.sound.add('sfx-gunshot', { volume: 0.8 }));
}
this.poolIndex = 0;
}
private fireGunshot(): void {
const snd = this.gunshotPool[this.poolIndex];
// Stop any currently playing instance at this slot, then play fresh
if (snd.isPlaying) snd.stop();
snd.play();
this.poolIndex = (this.poolIndex + 1) % this.gunshotPool.length;
}
声音池大小建议:与预期的最大重叠实例数一致。对于脚步声,通常 3–4 个就足够。
音频精灵图
音频精灵图将多个短声音打包到一个音频文件和一个 JSON 标记文件中。这可以减少 HTTP 请求,特别适合音频加载较慢的移动设备。
加载
// preload()
this.load.audioSprite(
'sfx-pack', // key
'assets/audio/sfx-pack.json', // JSON with marker definitions
['assets/audio/sfx-pack.mp3', 'assets/audio/sfx-pack.ogg'] // audio files
);
播放
// this.sound.playAudioSprite(key, markerName, config?)
this.sound.playAudioSprite('sfx-pack', 'coin');
this.sound.playAudioSprite('sfx-pack', 'jump', { volume: 0.8 });
this.sound.playAudioSprite('sfx-pack', 'hurt', { rate: 1.2 });
JSON 格式
{
"resources": ["sfx-pack.mp3", "sfx-pack.ogg"],
"spritemap": {
"coin": { "start": 0.0, "end": 0.4, "loop": false },
"jump": { "start": 0.5, "end": 0.85, "loop": false },
"hurt": { "start": 1.0, "end": 1.6, "loop": false },
"music": { "start": 2.0, "end": 34.0, "loop": true }
}
}
完整的 AudioSprite JSON 架构请参阅 references/audio-api.md。
音量管理
// Global volume (affects all sounds)
this.sound.volume = 0.5; // set
const vol = this.sound.volume; // get
// Per-sound volume
music.setVolume(0.4);
const soundVol = music.volume;
// Mute/unmute everything
this.sound.mute = true; // mute all
this.sound.mute = false; // unmute all
// Check if global mute is on
const isMuted = this.sound.mute;
静音按钮模式
create(): void {
const muteBtn = this.add.image(750, 30, 'btn-mute').setInteractive();
muteBtn.on('pointerdown', () => {
this.sound.mute = !this.sound.mute;
muteBtn.setTexture(this.sound.mute ? 'btn-unmute' : 'btn-mute');
});
}
解锁移动设备音频
移动浏览器和某些桌面浏览器会阻止音频播放,直到用户与页面交互。这是浏览器层面的限制,无法绕过。
Phaser 会自动处理。 它会监听首次 pointerdown 或 keydown 事件,并在此时恢复 AudioContext。在解锁之前排队等待的所有声音都会立即开始播放。
检查音频是否已锁定:
if (this.sound.locked) {
// AudioContext has not yet been unlocked
// Show a "tap to start" overlay
}
// Listen for the unlock event
this.sound.on(Phaser.Sound.Events.UNLOCKED, () => {
// Now safe to play audio
this.bgMusic.play();
});
对音频至关重要的游戏的最佳做法: 显示全屏“点击开始”遮罩层。玩家点击后将其关闭。Phaser 的内部解锁机制会同时触发,因此下一次调用 play() 时就会开始播放音频。
create(): void {
this.bgMusic = this.sound.add('music-main', { loop: true, volume: 0.6 });
if (this.sound.locked) {
const overlay = this.add.rectangle(400, 300, 800, 600, 0x000000, 0.7)
.setInteractive();
const label = this.add.text(400, 300, 'TAP TO START', {
fontSize: '32px', color: '#ffffff',
}).setOrigin(0.5);
this.sound.once(Phaser.Sound.Events.UNLOCKED, () => {
overlay.destroy();
label.destroy();
this.bgMusic.play();
});
} else {
this.bgMusic.play();
}
}
AudioContext 挂起后的恢复
移动浏览器(尤其是 iOS Safari)和一些桌面浏览器会在标签页失去焦点、设备休眠或 PWA 进入后台时挂起 AudioContext。与初次自动播放锁定不同,恢复不会自动进行——Phaser 不会在标签页重新获得焦点时恢复已挂起的上下文。
**症状:**用户切换标签页后再返回,音乐突然停止。控制台中没有错误。this.sound.locked 为 false(上下文之前已解锁),但音频仍然静音。
修复方法 — 在可见性变化时恢复上下文:
// In PreloaderScene.create() or main.ts, after game is initialized:
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
const mgr = this.sound as Phaser.Sound.WebAudioSoundManager;
if (mgr.context?.state === 'suspended') {
mgr.context.resume();
}
}
});
替代方法 — 使用 Phaser 内置的焦点事件:
this.game.events.on(Phaser.Core.Events.FOCUS, () => {
const mgr = this.sound as Phaser.Sound.WebAudioSoundManager;
if (mgr.context?.state === 'suspended') {
mgr.context.resume();
}
});
调用 resume() 前检查 mgr.context?.state——上下文已处于 'running' 状态时调用不会产生任何效果,但处于 'closed' 状态时调用会抛出错误。
AudioContext 状态:
| 状态 | 含义 |
|---|---|
'running' | 音频正常播放 |
'suspended' | 已暂停(标签页隐藏、设备休眠)——调用 resume() |
'closed' | 已永久关闭——创建新的游戏实例 |
在场景之间交叉淡化音乐
音乐突然中断会显得不够专业。在离开的场景中淡出旧曲目,在进入的场景中淡入新曲目。
// --- In OutgoingScene.shutdown() ---
shutdown(): void {
if (this.bgMusic?.isPlaying) {
this.tweens.add({
targets: this.bgMusic,
volume: 0,
duration: 500,
onComplete: () => this.bgMusic.stop(),
});
}
}
// --- In IncomingScene.create() ---
create(): void {
this.bgMusic = this.sound.add('music-new', { loop: true, volume: 0 });
this.bgMusic.play();
this.tweens.add({
targets: this.bgMusic,
volume: 0.6,
duration: 800,
ease: 'Linear',
});
}
注意:this.tweens 可以补间任何对象上的任意数值属性,包括 sound.volume。无需专用的音频补间 API。
场景关闭时停止声音
场景关闭时务必清理音频。否则,前一个场景中的声音会一直播放。
// Stop all sounds owned by the SoundManager (global — affects all scenes)
this.sound.stopAll();
// Stop only a specific sound
this.bgMusic.stop();
// Preferred pattern in shutdown():
shutdown(): void {
// If music belongs to this scene only
this.bgMusic?.stop();
// If this is a top-level scene and you want to silence everything:
// this.sound.stopAll();
}
仅在退出顶层游戏或切换到完全无关的游戏状态时使用 stopAll()。场景切换时,只停止当前场景所拥有的特定声音。
更多资源
参考文件
references/audio-api.md— SoundManager、BaseSound 和 WebAudioSound 的完整 API 参考,包含所有事件、SoundConfig 字段和 AudioSprite JSON 架构