マガジン

セーブしたのに再開すると最初から:保存のタイミングを疑う

遊びをつくる

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

プレイヤーの駒と小さなゲームの世界をつなぐ橋、上に浮かぶ雲形の器
記事のテーマをもとに制作したAI生成イラスト。

ステージ3まで進めてセーブしたはずのゲームを、ブラウザの再読み込みで開き直したらステージ1に戻っていた。そんな場面を考えてみます。画面には何のエラーも出ていません。この「最初から」という症状には、原因が少なくとも3つ重なっています。保存の処理がそもそも呼ばれていない、呼ばれたが別の場所に書いている、書いたものを読み込めていない、の3つです。 どれも見た目は同じ「最初から」になるので、手当たり次第にコードを直すと遠回りになります。この記事では、3つを順番に切り分ける考え方と、保存と読み込みの様子を画面に出す小さな診断コードを紹介します。アソバースの仕様のうち保存に関わる確認済みの部分と、公式作品の保存の有無から読み取れることも合わせて説明します。

この記事の目次
  1. 「最初から」に見える3つの原因
  2. 仕様どおりの「最初から」を除外する
  3. 切り分け1:保存が呼ばれていない
  4. 切り分け2:保存先が違う
  5. 切り分け3:読み込みが失敗している
  6. ログで3つを見分ける
  7. 次の行動

「最初から」に見える3つの原因

まず3つを並べて、それぞれ何を確かめれば区別できるかを整理します。

  • 保存が呼ばれていない、起きていること:節目で保存の処理が動いていない、確かめること:保存の呼び出しが記録に残るか
  • 保存先が違う、起きていること:書いてはいるが、再開時に読む場所に残らない、確かめること:書いた場所と読む場所が同じか、その場所が残る性質か
  • 読み込みが失敗、起きていること:データはあるが、読んだあとに状態へ反映されない、確かめること:読み込みの結果と、その後の上書きの有無

大事なのは、この順番で疑うことです。保存が呼ばれていないのに読み込み処理を直しても、何も変わりません。保存先を確認しないまま保存の回数を増やしても、書いた先が残らないなら同じ結果になります。

もう一つ、最初に除外したい可能性があります。「最初から」が仕様どおりの動作である場合です。

仕様どおりの「最初から」を除外する

アソバースの公式作品13本のうち、保存があるのは「余白の庭」「WAVE / WAVE」「かたちのあいだ」の3作品です。ほかの10作品の記録や録音は、画面を開いている間だけ残ります。たとえば「こだまの森」の最高記録、「雨音のピアノ」の録音、「月夜の配達」の各夜の最少区間数がそうです。これらは、閉じれば消える前提で作られていると読み取れます。

ここから、作る側に一つの判断基準が見えます。そのゲームは「閉じても残す」設計か、「開いている間だけ残す」設計か。後者のつもりなのに遊ぶ側が続きを期待するなら、バグではなく説明の問題です。画面に「記録は開いている間だけ」と書くだけで、問い合わせの多くは防げます。

自分のゲームが前者のはずなのに最初からになるなら、次に進みます。

切り分け1:保存が呼ばれていない

保存の処理は書いてあっても、呼ばれる契機が足りないことがよくあります。典型は次の3つです。

  • ページを離れる時のイベントだけに保存を任せている。タブを閉じる、画面を切り替えるといった場面で、そのイベントが必ず間に合うとは限りません。
  • クリアや終了の画面でしか保存していない。途中で閉じたら何も残りません。
  • 保存ボタンを押した時だけ保存し、ほかの操作では保存しない。

対策は、保存の契機をプレイの節目に置くことです。ステージ到達、アイテム取得、設定変更など、「ここまでは失いたくない」と判断できる単位で保存します。余白の庭では、置いたものの移動や大きさ調整、戻す/やり直すといった操作が続きます。こうしたゲームでは、操作が一段落するたびに保存するのが自然な設計の一つです。実際にどのタイミングで保存しているかは、作品を開いて確かめてください。

書き込みを増やしすぎるのも考えものです。クラウドセーブへの書き込みを毎フレーム行うような作り方が適切かどうかは、現在の案内で確認してください。

切り分け2:保存先が違う

書いたのに残らない原因として、意外に多いのが保存先の取り違えです。アソバースの確認済み事項から、次の点が押さえられます。

  • アップロード作品の中のlocalStorageとsessionStorageは、一時メモリーの互換処理です。ブラウザの通常の永続保存と同じ意味で、再読み込み後も残ると考えてはいけません。
  • 続きから遊べる保存にしたいなら、クラウドセーブを使います。作品ごとにJSONで128KiBまでです。
  • アップロードしたゲームにはSDKが自動で追加されます。URLで投稿するゲームは、作者がSDKを自分で読み込むと、作品専用のプレイヤーIDとクラウドセーブが使えます。
  • ゲームにFirebaseのUID、認証トークン、メールアドレスは渡さない決まりです。保存のキーに使うのも避けます。

つまり、ローカル環境でlocalStorageに保存して動いたゲームを、そのままアップロードして「再開できない」と困るケースは、ここに当たります。SDKの具体的な呼び出し方は、現在の案内で確認してください。

ログインの状態も見落としやすい点です。ゲストの履歴と進行は端末内に保存され、ログイン後のアカウントには自動では混ざりません。ゲストで進めたあとにログインして開くと、別の記録を見ているので「最初から」に見えます。逆に、未同期の端末セーブは、クラウドの古いデータより優先して再送されます。この仕組みの存在を知っておけば、「古いデータに戻った」という現象を調べるときに、端末側の状態から疑えます。

切り分け3:読み込みが失敗している

データが残っていても、読み込みの段階で失敗することがあります。確認したい点は次のとおりです。

  1. JSONの解釈に失敗していないか。 保存が途中で切れた、あるいは文字列以外を書いた場合などに起こります。
  2. データの版が合っているか。 公式作品では、古い保存(version1)を新しい形式(version2)へ移行する仕組みがあるとされています。自作でも、版の番号をデータに含め、古い版を読んだら変換する分岐を最初から用意しておくと安全です。
  3. サイズが上限を超えていないか。 クラウドセーブは128KiBまでです。超えた場合の挙動は、現在の案内で確認してください。
  4. 読み込む前に初期状態を保存していないか。 起動処理の中で初期状態を保存する処理が先に動くと、せっかくのデータを上書きします。この場合、読み込みは失敗ではなく、成功したあとに上書きされた形になります。

4つ目は、保存の問題に見えて読み込みの順序の問題という、紛らわしい例です。見分けには、保存と読み込みが動いた順番を記録するのが効果的です。

ログで3つを見分ける

最後に、保存と読み込みの様子を画面に出す最小のコードを示します。保存先はstoreにまとめてあるので、クラウドセーブに差し替えるときも、ここだけを入れ替えれば同じログで確かめられます。

html
<!doctype html>
<meta charset="utf-8">
<pre id="log"></pre>
<button id="inc">進める</button>
<script>
const log = m => { document.getElementById('log').textContent += m + '\n'; };
const KEY = 'diag-save';
const store = {
  write(text) {
    try { localStorage.setItem(KEY, text); }
    catch (e) { log('[store] 書き込み失敗: ' + e.message); }
  },
  read() {
    try { return localStorage.getItem(KEY); }
    catch (e) { log('[store] 読み出し失敗: ' + e.message); return null; }
  }
};
let state = { version: 2, stage: 1 };

function save(reason) {
  const text = JSON.stringify(state);
  log('[save] ' + reason + ' ' + text.length + '文字');
  store.write(text);
}
function load() {
  const text = store.read();
  if (text === null) { log('[load] 保存なし。初期状態で開始'); return; }
  try {
    const data = JSON.parse(text);
    if (data.version !== 2) { log('[load] version違い: ' + data.version); return; }
    state = data;
    log('[load] 成功 stage=' + state.stage);
  } catch (e) { log('[load] 解釈失敗: ' + e.message); }
}
load();
document.getElementById('inc').onclick = () => {
  state.stage++; log('stage=' + state.stage); save('ボタン');
};
</script>

使い方は次のとおりです。

  1. ボタンを何回か押してstageを進める。
  2. 画面を再読み込みして、ログの先頭に出る[load]の行を見る。
  3. 自作ゲームでは、同じ形で[save]と[load]の行を出す。保存先は実際に使うものに差し替える。

ログの読み方は次のとおりです。

  • 進めたのに[save]が1行も出ない → 保存が呼ばれていません。 切り分け1に戻って、呼び出しの契機を見直します。
  • [save]は出るが、再読み込み後が[load] 保存なし → 保存先が残らない場所です。 切り分け2です。このサンプルのlocalStorageは一時メモリーの互換処理にあたるので、アップロードした環境で試すと、この行になる可能性があります。結果は読者の環境で確かめてください。
  • [load]の行がversion違いや解釈失敗 → 読み込みの失敗です。 切り分け3です。
  • [load] 成功のあとに、[save]で初期値が書かれている → 読み込み後の上書きです。 起動処理の順序を直します。

見るのは「出たか」「何の順に出たか」の2点です。画面を何度か開き直してもログの形が変わらないことを確かめれば、原因の候補は大きく絞れます。ゲストとログイン、両方の状態で同じ手順を行うと、切り分け2のアカウント側の差も確認できます。

次の行動

自分のゲームに、[save]と[load]のログ行を足してください。そのうえで「進める→再読み込み」を、ゲストとログインの両方で行います。記録に残るのは保存の呼び出し、書き込み先、読み込みの結果、上書きの順序です。どこで途切れているかが分かれば、直す場所は一つに絞れます。