マガジン

ブラウザの戻るボタンでゲームが消える:履歴の扱い方

遊びをつくる

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

しおり・ハート・時計の目印が付いた棚から、小さなゲーム世界の本を取り出すイラスト
記事のテーマをもとに制作したAI生成イラスト。

ゲームをプレイ中に誤って戻るボタンを押したとたん、画面が前のページに飛んでしまい、進行中のデータがすべて消える——これはブラウザゲーム特有のトラブルのなかでも、プレイヤーの離脱につながりやすい問題です。「ゲームの途中で戻るボタンを押さないでください」と説明文に書いても、習慣的に押してしまう人は必ずいます。 原因は、ブラウザの「戻る」操作がページ遷移と同じ扱いになっていることです。ゲームがシングルHTML・シングルページ構成であっても、ブラウザ自体はその事情を知りません。操作を受け取るとそのまま履歴を一つさかのぼります。この動作を制御するには、History API(history.pushState / history.replaceState)と popstate イベントを組み合わせるのが現実的な手段です。 この記事では、戻るボタン問題が起きる仕組みを整理したうえで、対処の選択肢とそれぞれの判断基準、実装の要点を説明します。エンジンやフレームワークに依存しない考え方を軸にするので、素のHTMLで作ったゲームにも、ツールで書き出したゲームにも応用できます。

この記事の目次
  1. なぜ戻るボタンがゲームを壊すのか
  2. 三つの対処方針と選び方
  3. History API を使った実装の要点
  4. `localStorage` でセーブするときの設計ポイント
  5. ゲームジャンルごとの判断基準
  6. 次の行動
  7. 参考資料

なぜ戻るボタンがゲームを壊すのか

ブラウザはページを開くたびに「セッション履歴」というスタックを積み上げます。ユーザーがリンクをたどるか、JavaScriptが location.href を書き換えるたびにエントリが追加され、戻るボタンはその一つ前のエントリに移動します。

シングルページのゲームでは通常、ページ遷移は起きていません。しかしゲームを開いたページ自体の「前の履歴」は存在します。たとえばゲーム一覧からゲームを開いた場合、履歴スタックには「一覧ページ → ゲームページ」という順番が積まれています。戻るボタンを押せば一覧に戻るのはブラウザとしては正しい動作です。

問題は二重になっています。一つはページ遷移でゲーム状態がリセットされること。もう一つは、「戻る」を押した本人が意図していない場合がある、という点です。マウスのサイドボタン、キーボードの Alt+←、スマートフォンのスワイプジェスチャーなど、戻る操作には複数の入力経路があるため、誤作動は思ったより頻繁に起きます。

三つの対処方針と選び方

対処には大きく三つの方向があります。どれが適切かはゲームの性質によって変わります。

方針A:戻る操作をその場で「吸収」する

History API の history.pushState を使い、ゲーム起動時に履歴エントリを一つ追加します。すると戻るボタンを押したときにブラウザが前のページではなく「追加したエントリ」に移動し、popstate イベントが発火します。そのイベントを受け取って「本当に終了しますか?」のダイアログを出すか、何もせずに history.pushState でエントリを再度追加して現在の状態を保持します。

この方法はゲームがページ遷移なしで完結するシングルページ構成に向いています。プレイヤーが戻るボタンを押してもゲームを抜けさせない、という要件を最もシンプルに満たせます。

方針B:`beforeunload` イベントで離脱を警告する

window.addEventListener('beforeunload', ...) を登録すると、ページを離れる直前にブラウザが確認ダイアログを表示します。ユーザーが「このページを離れますか?」に「はい」と答えた場合だけ遷移が進みます。

簡単に書けるのが利点ですが、欠点も明確です。モバイルブラウザの多くは beforeunload のダイアログを表示しないか、表示内容をカスタマイズできません。またカジュアルなゲームで頻繁に確認ダイアログが出ると、体験として重く感じられます。「ゲームオーバー後に終了する」「タイトルに戻る」などの明確な出口がある場面以外では過剰になりがちです。

方針C:ゲーム状態を随時保存して「消えても再開できる」設計にする

そもそもページが離れても進行が失われないなら、戻るボタンの脅威は大きく減ります。localStorage にゲームの状態を逐一保存し、次にページを開いたときに復元するという設計です。

この方針はセーブ機能の実装を前提とするため手間はかかりますが、戻るボタン以外のブラウザクラッシュやタブの誤閉じにも同時に対応できます。長時間かけてプレイするパズルや育成系のゲームでは、この方針が最も根本的な解決になります。

History API を使った実装の要点

方針Aを実装するときに押さえておくべきポイントを整理します。

ゲームの初期化処理のなかで、次の2行を実行します。

js
history.replaceState({ game: true }, '');
history.pushState({ game: true }, '');

replaceState は現在のエントリを上書きし、pushState で新しいエントリを積みます。これにより、戻るボタンが押されたとき「新しいエントリ → 元のエントリ」という遷移が起き、ページを離れずに popstate イベントだけが発火します。

イベントのハンドラは次のように登録します。

js
window.addEventListener('popstate', (e) => {
  if (e.state && e.state.game) {
    // ゲームを続けさせたい場合:エントリを再度追加して戻り操作を無効化
    history.pushState({ game: true }, '');
  } else {
    // ゲームの外に出た場合の処理(任意)
  }
});

注意点が二つあります。一つは、pushState はオリジン(スキーム+ドメイン+ポート番号)が同じURLにしか使えないという制限です。file:// で開いたローカルHTMLでは動作が安定しないため、開発中にローカルサーバーを使うか、実際のホスティング環境でテストしてください。もう一つは、スマートフォンのブラウザでスワイプバックを完全に止めることはできない点です。SafariのスワイプジェスチャーはJavaScriptで制御できない部分があり、popstate が発火する前にアニメーションが始まるケースがあります。これはiOSの仕様に起因するため、方針Cのセーブ設計と組み合わせるのが現実的です。

`localStorage` でセーブするときの設計ポイント

方針Cを選ぶ場合、セーブのタイミングと粒度が重要です。

セーブすべき最小単位は「失われたら取り返しがつかないデータ」です。たとえばアクションゲームなら現在のステージ番号とスコア、パズルなら盤面の状態と経過時間が相当します。毎フレーム保存するのは不要で、「ステージクリア時」「一手指すたび」「10秒ごと」など、ゲームの区切りに同期するのが効率的です。

キーの設計はシンプルにしておくと管理しやすくなります。gameName_saveData のように名前空間を設けておくと、同じブラウザで複数ゲームを使うプレイヤーの環境でも衝突を防げます。

データは JSON.stringify でシリアライズして保存し、読み込み時は JSON.parse で復元します。このとき、保存データの「バージョン番号」をオブジェクトに含めておくことをおすすめします。ゲームをアップデートしてデータ構造が変わったとき、古いデータとの互換性をバージョン番号で判断できます。バージョンが合わなければ初期化する、という処理を入れておくと、古いセーブデータが原因で起動エラーが出るトラブルを防げます。

ゲームジャンルごとの判断基準

  • アクション・リズム・カジュアルゲーム:1プレイが短く、途中セーブの意味が薄い。方針Aで戻る操作を吸収するだけで十分なことが多い。
  • パズル・謎解き・育成ゲーム:進行が長くセーブが重要。方針Cを軸にしつつ、方針Aを補助として組み合わせる。
  • マルチステップのフォームやチュートリアルを持つゲーム:ステップ間を履歴で管理する設計を取ると、ブラウザの戻るボタンを「前のステップに戻る」機能として活用できる。その場合は意図的に pushState でエントリを積み、popstate で前のステップを表示する。

次の行動

まずゲームのプレイ中に戻るボタンを実際に押し、どのような状態になるかを確認してください。ページが離れるだけなのか、ゲーム内の状態もリセットされるのかによって、優先すべき対処が変わります。短時間で試すなら history.pushState と popstate の組み合わせから始め、長く遊ぶゲームなら localStorage によるセーブ設計の検討に進んでください。作ったゲームをアソバースに投稿する場合も、プレイヤーの操作環境はさまざまなので、どちらの対策も実装しておくと安心です。

参考資料

  • MDN Web Docs「History API」
  • MDN Web Docs「Window: popstate イベント」
  • MDN Web Docs「Window: beforeunload イベント」
  • MDN Web Docs「Web Storage API(localStorage)」