マガジン

ゲーム音の読み込みを速くする:プリロードと遅延読み込み

遊びをつくる

執筆にClaudeを使用しています。

同じ絵の厚いカード束と薄い束を比べ、画像容量の最適化を表した写真風イメージ。
記事のテーマをもとに制作したAI生成イラスト。

ゲームを開いた瞬間にBGMが鳴らない、効果音が遅れて再生される――こうした問題の多くは、音ファイルの読み込みタイミングが原因です。画像と違い、音ファイルはブラウザが「デコードして再生できる状態」にするまでに時間がかかります。そのため、ボタンを押した瞬間に音を鳴らそうとしても、ファイルがまだ到着していなければ無音になります。 この問題を解決する手段は大きく二つあります。ゲーム開始前にまとめて読み込む「プリロード」と、必要になる直前まで読み込みを後回しにする「遅延読み込み(レイジーロード)」です。どちらが正解かは一律に決まらず、ゲームの構成と音ファイルの総量によって選ぶべき戦略が変わります。この記事では、判断基準と実装の手順を具体的に示します。

この記事の目次
  1. なぜ音の読み込みは画像より難しいのか
  2. プリロードと遅延読み込みの使い分け
  3. Web Audio APIを使ったプリロードの実装手順
  4. 遅延読み込みの実装で失敗しやすい点
  5. 実際に選ぶときの判断フロー
  6. 次の行動
  7. 参考資料

なぜ音の読み込みは画像より難しいのか

ブラウザで画像を表示する場合、<img> タグにパスを書けば多くの場面でそのまま表示されます。しかし音は、ファイルをダウンロードするだけでは不十分で、ブラウザが内部でオーディオデータとしてデコードする処理が挟まります。この処理はファイルサイズや形式によって数十ミリ秒から数百ミリ秒かかることがあり、タイミングがずれると「ボタンを押したのに音が出ない」という体験につながります。

また、ブラウザのオートプレイポリシーによって、ユーザーの操作(タップやクリック)が起きる前にオーディオコンテキストを開始できない制約があります。これはプリロードの完了とは別の問題で、「ファイルは読み込まれているのに音が鳴らない」という混乱の原因になりがちです。プリロードと遅延読み込みの話をする前に、オーディオコンテキストの開始は必ずユーザー操作のイベントハンドラ内で行う、という前提を押さえておくことが重要です。

プリロードと遅延読み込みの使い分け

プリロードが向く場面

プリロードは、ゲーム開始前のロード画面でまとめて音ファイルを取得・デコードしておく方式です。再生のタイミングで待ち時間がゼロになるため、操作と音がぴったり合うことが最大の利点です。

向いているのは次のような構成です。

  • 効果音の種類が少ない(目安として10〜20ファイル以下)
  • BGMが1曲か2曲で、ゲーム全体を通して使い続ける
  • モバイルより先にPC・高速回線を想定している
  • ロード画面を設けても違和感のないゲームジャンル(RPGやパズルなど)

逆に、音ファイルの合計サイズが数MBを超える場合はプリロードの待ち時間が長くなりすぎ、ユーザーがゲームを開く前に離脱するリスクが高まります。

遅延読み込みが向く場面

遅延読み込みは、実際に音が必要になる直前に初めてファイルを取得する方式です。初回ロードが軽くなるためゲームを素早く開始できますが、初めてその音を鳴らすときに一瞬の遅延が生じる可能性があります。

向いているのは次のような構成です。

  • 音ファイルの種類が多く、全部を最初に読み込むと重い
  • ステージやシーンが分かれており、「次のシーンで使う音」を事前に予測できる
  • ゲームの序盤では使わない効果音(ボスBGM、エンディング音楽など)がある
  • モバイル・低速回線のユーザーを意識している

遅延読み込みの弱点は「初回再生時のラグ」ですが、シーン遷移のアニメーション中など、音が必要になる少し前のタイミングで読み込みを開始する「プリフェッチ」と組み合わせることで実用上の問題を減らせます。

Web Audio APIを使ったプリロードの実装手順

Web Audio APIは、ブラウザ標準のオーディオ処理インターフェースです。以下は典型的なプリロード実装の考え方を示します。

1. AudioContextを用意する

AudioContext はユーザー操作のタイミングで初めて生成または再開します。new AudioContext() 自体はページ読み込み時に書いてもよいですが、.resume() の呼び出しはクリック・タップのイベント内で行います。

2. fetchしてArrayBufferで受け取る

fetch(url) でファイルを取得し、.arrayBuffer() でバイナリデータとして扱います。この段階ではまだ再生できません。

3. decodeAudioDataでデコードする

audioContext.decodeAudioData(arrayBuffer) を呼ぶと、ブラウザがデータを再生可能なオーディオバッファに変換します。このステップが終わって初めて即時再生が可能になります。デコードは非同期処理なので、await または .then() で完了を待ちます。

4. バッファをオブジェクトに保存しておく

デコード済みの AudioBuffer を { bgm: buffer1, jump: buffer2, ... } のようなオブジェクトに格納しておきます。再生時はここから取り出して BufferSourceNode に渡すだけなので、遅延なく鳴らせます。

5. ロード進捗をUIに反映する

複数ファイルをプリロードするときは、完了した数 / 総数をロードバーに反映します。Promise.all() で全ファイルのデコードを待つか、1ファイル完了ごとにカウンタを進める方法があります。ユーザーに待ち時間の目安を見せることで離脱を減らせます。

遅延読み込みの実装で失敗しやすい点

遅延読み込みは「必要になったら読む」という単純な発想ですが、実装で陥りやすい問題がいくつかあります。

初回再生時のラグを過小評価する

ファイルのフェッチ+デコードに要する時間は、回線速度とファイルサイズ次第で大きくばらつきます。「たぶん速い」と思っていると、低速回線の環境で効果音がまったく間に合わなくなります。重要な効果音(ヒット音、死亡音など)は遅延読み込みではなくプリロードに回す、またはシーン遷移前のわずかな時間を使って先読みするのが現実的な対応です。

同じファイルを何度も読み込んでしまう

遅延読み込みの実装で「再生するたびにfetchを走らせる」コードを書いてしまうケースがあります。一度デコードしたバッファはキャッシュオブジェクトに保存し、次回以降はそこから返すように実装してください。

AudioContextのsuspended状態に気づかない

遅延読み込みでは音が必要になるタイミングが操作から離れることがあります。オートプレイポリシーによってAudioContextが suspended 状態のままになっていると、バッファが用意できていても音が出ません。再生処理の直前に audioContext.state === 'suspended' を確認し、必要なら .resume() を呼ぶ処理を入れておくと安全です。

モバイルでのメモリ使用量

デコード済みのAudioBufferはエンコード前より大きなメモリを使います。長い曲を複数プリロードするとメモリ不足でアプリがクラッシュすることがあります。BGMのような長尺ファイルは <audio> 要素でストリーミング再生し、効果音だけをWeb Audio APIでバッファ管理するハイブリッド構成が現実的です。

実際に選ぶときの判断フロー

どちらを使うか迷ったときは、次の順序で考えると整理しやすいです。

  1. 音ファイルの合計サイズを計算する。 圧縮後のファイル合計が1MB未満ならプリロードで問題になることは少ないです。数MB以上になるなら遅延読み込みかハイブリッドを検討します。
  2. ゲームにロード画面を設けられるかを確認する。 ロード画面を自然に挟めるジャンルならプリロードの待ち時間をユーザーに受け入れてもらいやすいです。
  3. シーン構成で音を分類する。 「序盤で必ず使う」「後半だけで使う」に分けられるなら、前者はプリロード、後者はシーン遷移時の先読みに割り振ります。
  4. 効果音と長尺BGMを分けて扱う。 効果音はWeb Audio APIでバッファ管理、BGMは<audio>要素のストリーミングという組み合わせが、メモリとレスポンスの両立に有効です。

次の行動

手元のゲームで音ファイルの合計サイズを確認し、1MBを超えるようなら効果音と長尺BGMを分けてリストアップするところから始めてください。実装前に「どの音が最初から必要か」を整理するだけで、プリロードと遅延読み込みの振り分けがほぼ決まります。作ったゲームをアソバースに投稿する際も、ZIPにまとめて音ファイルを含めたまま公開できるため、読み込み戦略を決めてから最終ビルドを作るとスムーズです。

参考資料

  • MDN Web Docs — Web Audio API
  • MDN Web Docs — AudioContext.decodeAudioData()
  • MDN Web Docs — Autoplay policy in browsers(Chrome・Firefoxの公式ドキュメント)