ブラウザゲームのセーブ機能:localStorageの使い方と限界
遊びをつくる執筆にClaudeを使用しています。

ブラウザゲームを作るとき、最初に壁になりやすいのがセーブ機能です。プレイヤーがページを閉じても進行状況を保持するには、何らかの方法でデータをブラウザ側に残す必要があります。その手段として最も手軽に使えるのが localStorage です。 localStorage はJavaScriptだけで読み書きでき、サーバーもデータベースも不要です。キーと値のペアをブラウザに永続的に保存でき、同じオリジン(ドメイン+プロトコル+ポートの組み合わせ)であればページをまたいで読み取れます。ゲームの初期セーブ機能としては十分な能力がありますが、容量・構造・セキュリティの面で無視できない制約もあります。 この記事では、localStorage の基本的な使い方から、ゲームデータに特有の設計パターン、そして「これ以上は localStorage では無理」と判断する目安まで、順番に説明します。
この記事の目次
localStorageの読み書き:最小限のコード
localStorage の操作は4つのメソッドで完結します。
// 保存
localStorage.setItem('saveData', JSON.stringify({ level: 3, hp: 80 }));
// 読み込み
const raw = localStorage.getItem('saveData');
const save = raw ? JSON.parse(raw) : null;
// 削除(1件)
localStorage.removeItem('saveData');
// 全消去
localStorage.clear();値は文字列しか保存できないため、オブジェクトや配列を扱うときは JSON.stringify で文字列化し、読み込み時に JSON.parse で復元します。getItem は存在しないキーに対して null を返すので、初回起動かどうかの判定にも使えます。
注意点は JSON.parse の例外処理です。保存データが壊れていたり、別バージョンのゲームが書いた形式と一致しなかったりすると、JSON.parse はエラーを投げます。実用コードでは try…catch で囲み、読み込み失敗時は初期値にフォールバックする設計にしておくと安全です。
function loadSave() {
try {
const raw = localStorage.getItem('saveData');
return raw ? JSON.parse(raw) : getDefaultSave();
} catch {
return getDefaultSave();
}
}ゲームデータの設計:何をどう保存するか
localStorage に何でも詰め込むと、後からデータ構造を変えたときに過去セーブとの互換性が崩れます。最初から「保存するキー」「値の構造」「バージョン番号」を決めておくと管理が楽になります。
バージョン番号を持たせる
セーブデータのオブジェクトにバージョン番号を入れておくと、ゲームのアップデートで構造が変わったときに移行処理を書けます。
const DEFAULT_SAVE = {
version: 1,
level: 1,
coins: 0,
unlockedStages: [1],
};読み込み時に save.version < CURRENT_VERSION を確認し、古ければマイグレーション関数を呼ぶ構造にしておくと、プレイヤーの進行状況を壊さずにデータ形式を更新できます。
複数スロットを作る場合
セーブスロットを複数用意したいときは、saveSlot_1・saveSlot_2 のようにキーにスロット番号を含めるのが単純です。スロット一覧は別キー(saveIndex など)に配列として保存しておくと、ロビー画面で列挙しやすくなります。
頻繁に書き込む値に注意する
localStorage.setItem は同期処理です。毎フレーム呼び出すとパフォーマンスに影響します。プレイ中は変数やオブジェクトで状態を管理し、ステージクリア・ポーズ・画面遷移などの区切りでまとめて保存するのが実用的なパターンです。
localStorageの限界:数字と構造で知っておくこと
localStorage には無視できない制約があります。問題が起きてから気づくより、設計段階で把握しておくほうが手戻りを減らせます。
容量の上限
ブラウザごとに差がありますが、オリジンあたりおよそ5MBが目安です。テキストデータとしてのセーブ情報(レベル・フラグ・スコアなど)であれば通常は問題ありませんが、マップエディタのカスタムデータや大量のアンロック状態を保存しようとすると上限に近づく場合があります。容量超過時は setItem が QuotaExceededError を投げるので、保存処理の try…catch 内でこのエラーを検知してプレイヤーに通知できます。
デバイスとブラウザをまたげない
localStorage はそのデバイス・ブラウザ・オリジンに閉じています。スマートフォンで進めたデータをPCで続けることも、ブラウザを変えてアクセスすることもできません。スマホとPCの両対応や「どこからでも同じ進行状況でプレイ」が要件に入るなら、クラウドへのデータ同期が必要になります。
プレイヤーが削除できる
ブラウザの設定やキャッシュ削除でデータが消えます。ゲームの性質上「消えても痛くないデータ(設定値・ハイスコア)」と「消えると体験が壊れるデータ(ストーリー進行・課金アイテム)」を区別して、後者は別の保存手段を用意するか、消えた場合の案内を画面に出しておくことが大切です。
プライベートブラウジングの挙動
プライベートモード(シークレットウィンドウ)では、localStorage に書き込めてもウィンドウを閉じると消えるブラウザがあります。「保存したはずのデータが消えた」という報告の多くはこのケースです。仕様上避けられないため、プライベートモードで挙動が変わる旨を説明する場所を設けておくと丁寧です。
sessionStorageとの使い分け
sessionStorage は localStorage と同じAPIを持ちますが、タブを閉じると消えます。ゲームでの用途は「ステージ間の一時的な状態の受け渡し」や「プレイ中だけ保持したいフラグ」です。セーブデータとしてではなく、ページ遷移をまたぐ短命なバッファとして使うと整理がつきやすいです。
「ゲームを途中で離脱しても再開できる」ためのセーブは localStorage、「ゲームセッション内だけ有効な情報」は sessionStorage と役割を分けると、保存領域が混在せずコードの見通しもよくなります。
「localStorageでは足りない」と判断する目安
次のいずれかに当てはまる場合は、localStorage の外にある仕組みを検討する段階です。
- 複数のデバイスで同じセーブデータを使いたい
- セーブデータを消させたくない(誤操作・キャッシュ削除で失われては困る)
- プレイヤーごとのデータをサーバー側で集計・比較したい(ランキングなど)
- 保存データが5MB近くなる可能性がある
これらの要件が出てきたときは、IndexedDB(より大きなブラウザ内ストレージ)やサーバーサイドのAPI・データベース、あるいはアソバースのSDKが提供するクラウドセーブ機能が選択肢になります。localStorage はあくまで「小規模・単一デバイス・消えても再現可能」なデータに向いた道具です。
次の行動
まずは localStorage.setItem と JSON.stringify を組み合わせた最小限のセーブ・ロード処理を自分のゲームに組み込み、ページを閉じて再度開いてもデータが残ることを確認してみてください。その後、バージョン番号の付与と try…catch によるフォールバックを追加すると、実用に耐える堅牢さになります。複数デバイス対応やデータの永続性が必要になった時点で、クラウドセーブへの移行を検討してください。アソバースのSDKを使ったクラウドセーブについては、公式ドキュメントで対応条件を確認してください。
参考資料
- MDN Web Docs「Web Storage API」
- MDN Web Docs「localStorage」
- MDN Web Docs「JSON.stringify / JSON.parse」

