セーブデータの引き継ぎコードの作り方
遊びをつくる執筆にClaudeを使用しています。

引き継ぎコードとは、プレイヤーの進行状況をテキスト文字列に変換し、別の端末やブラウザで入力することでセーブデータを復元する仕組みです。クラウドセーブやアカウント連携が用意できない状況でも、プレイヤーが自分のデータを持ち出せるようにする手軽な代替手段として、ブラウザゲームでは今も広く使われています。 「データをどこに保存するか」という問いとは別に、「そのデータをどうやって文字列に変換し、また元に戻すか」という変換の設計が引き継ぎコードの核心です。この記事では、設計の考え方から実装の手順、失敗しやすい落とし穴まで、具体的に説明します。
引き継ぎコードが必要になる場面を整理する
ブラウザゲームのセーブは多くの場合 localStorage に頼ります。これは同じブラウザ・同じオリジンで動く限り便利ですが、「スマホからPCに移りたい」「プライベートモードでプレイしていた」「ブラウザのデータを消してしまった」などのケースで失われます。
引き継ぎコードはこの弱点を補うために作ります。コードを生成してメモしておけば、どの端末・どのブラウザでも同じデータを再現できます。サーバーを用意しなくてよいため、個人開発者にとってコストゼロで実装できる点も魅力です。
一方で、引き継ぎコードに向かない用途もあります。リアルタイムのスコアランキングや、サーバー側で検証が必要な課金アイテムの保持には不向きです。チート対策を本格的に行いたい場合は、サーバー連携か、後述する署名の仕組みが必要になります。
保存するデータを絞り込む
引き継ぎコードを短く・壊れにくくするには、「何を引き継ぐか」を最初に決めることが最重要です。ゲームの全内部状態をそのまま文字列にしようとすると、コードが数百文字になり、入力ミスが増えます。
引き継ぐべき最小セットを考えるときの基準は次の通りです。
- 再計算できるものは含めない。 HPの上限や攻撃力が「レベルから計算される」なら、レベルだけ保存すれば足ります。
- 揮発性の状態は含めない。 現在地やバトル中の一時状態は引き継ぎ対象にする必要がありません。
- バージョン番号は必ず含める。 ゲームのアップデートでデータ構造が変わったとき、古いコードを正しく処理するために不可欠です。
たとえばRPGなら「バージョン・プレイヤー名・レベル・所持アイテムのIDと個数・フラグ一覧」程度に絞れることが多いです。この絞り込みがコードの品質を決めます。
変換の実装手順
1. データをオブジェクトにまとめる
const saveData = {
v: 1, // バージョン
name: "ユーザー名",
lv: 24,
items: { "sword": 1, "potion": 3 },
flags: [1, 0, 1, 1, 0] // クリア済みフラグなど
};2. JSON文字列 → Base64 に変換する
JSON.stringify でテキスト化し、さらに btoa / atob でBase64エンコードします。日本語など非ASCII文字が含まれる場合は encodeURIComponent を挟む必要があります。
function encode(obj) {
const json = JSON.stringify(obj);
return btoa(encodeURIComponent(json));
}
function decode(code) {
try {
const json = decodeURIComponent(atob(code));
return JSON.parse(json);
} catch {
return null; // 不正なコードはnullで返す
}
}Base64の出力には + / = が含まれます。これらは入力欄に貼るだけなら問題ありませんが、URLに埋め込む場合は btoa の代わりに URL-safe Base64(+→-、/→_、= 除去)に変換する処理を加えてください。
3. コードを短くしたい場合は圧縮を検討する
データ量が多いときは、Base64の前に pako(zlib/deflateのJSポート)などのライブラリで圧縮できます。ただし依存ライブラリが増えるため、まずは非圧縮で試して、コードが長すぎると判断してから導入するほうが実装はシンプルに保てます。
4. バージョン処理を書く
function loadFromCode(code) {
const data = decode(code);
if (!data) return { error: "コードが正しくありません" };
if (data.v === 1) return migrateV1(data);
if (data.v === 2) return data; // 現行バージョン
return { error: "このコードは古すぎます" };
}migrateV1 のような移行関数を用意しておくと、構造変更のたびに旧コードを読み捨てずに済みます。
よくある失敗と対処法
改行・空白が混入する。 テキストエリアからコードを読み込むとき、前後の空白や改行が混ざることがあります。trim() をかけてから decode に渡す習慣をつけてください。
日本語名が化ける。 btoa はASCII外の文字を直接扱えません。前述の encodeURIComponent を挟む処理を省くと、日本語のプレイヤー名がエラーになります。
コードが長くなりすぎて入力ミスが増える。 40〜50文字を超えるあたりから手入力のミスが増えます。コピー&ペーストを前提にUIを設計し、「コードをコピー」ボタンを用意するだけで体験が大きく改善します。
バージョン番号なしで公開してしまう。 後から追加しようとすると、既存ユーザーの古いコードを全員エラー扱いにするしかなくなります。最初のバージョンから必ず v: 1 を含めてください。
チートへの対処。 Base64は可逆変換なので、プレイヤーが値を書き換えて読み込むことは技術的に可能です。カジュアルゲームであれば許容範囲ですが、スコアの正当性を保ちたい場合は、データに対してHMAC(crypto.subtle.sign)で署名し、読み込み時に検証する方法があります。秘密鍵をクライアントに置く以上、完全なチート対策にはなりませんが、単純な改ざんを防ぐ抑止力にはなります。
UIの設計で引き継ぎ体験を整える
実装が完成しても、UIが不親切だとプレイヤーに使ってもらえません。以下の点を確認してください。
- 生成ボタンと入力欄を別の画面またはタブに分ける。 生成と読み込みが同じ場所にあると、誤って上書きするミスが起きます。
- 読み込み前に確認ダイアログを挟む。 「現在のデータは上書きされます。よろしいですか?」の一言でトラブルが減ります。
- エラーメッセージを具体的にする。 「コードが正しくありません」だけでなく、「バージョンが古いためこのコードは使えません」のように原因を示すと、プレイヤーが対処できます。
- コードの有効期限をドキュメントに明記する。 ゲームのバージョンアップで古いコードが使えなくなる可能性があることをあらかじめ伝えておくと、クレームを減らせます。
次の行動
引き継ぎコードの実装は、encode / decode の2関数と、バージョン分岐の処理を書くところから始められます。まず手元のゲームで「引き継ぐ最小セット」を書き出し、小さなHTMLで動作確認してみてください。動作確認ができたゲームはアソバースにZIPまたはHTMLファイルとして投稿・公開できます。
参考資料
- MDN Web Docs — btoa / atob リファレンス
- MDN Web Docs — Web Crypto API(crypto.subtle.sign)
- pako ライブラリ公式ドキュメント

