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
Phaser は、対応しているすべてのブラウザー(すべてのモダンブラウザー)でデフォルトで Web Audio を使用します。Web Audio は専用スレッドでオーディオ処理を実行し、空間オーディオ、エフェクトチェーン、精密なスケジューリングをサポートし、HTML5 Audio を悩ませる単一トラック制限の影響を一切受けません。
HTML5 Audio は、AudioContext が利用できない場合の自動フォールバックです。典型的には古い Android WebView や、一部のエッジケースのブラウザー構成が該当します。意図的にこれをターゲットにすることはほとんどありません。強制的に使用する必要がある場合は次のとおりです。
const config: Phaser.Types.Core.GameConfig = {
audio: {
disableWebAudio: true, // force HTML5 Audio fallback
},
};
特定の互換性要件がない限り、決して HTML5 Audio を強制しないでください。 単一の同時トラック、detune/rate 非対応、空間配置非対応といったその制限が、設計を縛ることになります。
オーディオの読み込み: 常に 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']);
}
単一のフォーマットだけを読み込まないでください。Chrome の開発マシンでは完璧に動作するゲームも、.ogg しか出荷していなければ 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 });
経験則: BGM → add()。単発の SFX → sound.play()。
SoundConfig のオプション
SoundConfig オブジェクトを add() の第 2 引数として、または play() の第 2 引数として渡します。
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)
};
BGM
BGM は連続的にループし、音量制御が必要で、シーン終了時に停止しなければなりません。
// --- 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();
SFX のパターン: 単発サウンド
コインの取得、銃声、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 は内部インスタンスを生成し、最後まで再生してから破棄します。クリーンアップは一切不要です。
高速な SFX のためのサウンドプーリング
1 秒間に何度も再生されるサウンド(銃声、足音、素早い 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 で十分です。
オーディオスプライト
オーディオスプライトは、複数の短いサウンドを 1 つのオーディオファイルと 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 スキーマ