mcpField
P
phaser-audio

phaser-audio

github.com/yakoub-ai开发29194 次安装9 周前更新
代码仓库

关于 phaser-audio

phaser-audio 是面向 Phaser 4 的浏览器游戏音频技能,介绍音频后端选择,以及资源加载、播放和生命周期管理等实用模式。

主要功能

  1. 01Web Audio 与 HTML5 Audio 指引
  2. 02MP3 和 OGG 资源加载
  3. 03音乐与单次音效模式
  4. 04声音池与音频精灵
  5. 05音量、解锁与恢复处理
  6. 06场景清理和音乐过渡

适用场景

  1. 01选择音频后端和兼容格式
  2. 02添加循环音乐与单次音效
  3. 03处理快速重复触发的音效
  4. 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 架构

评论

支持 Markdown