マガジン

クラウドセーブの実装:同期の衝突をどう解決するか

遊びをつくる

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

スマートフォンとノートパソコンの枠を、赤いゲームの駒と雲のアーチがつなぐ切り絵
記事のテーマをもとに制作したAI生成イラスト。

クラウドセーブを実装した直後に待ち受ける最初の難問が「同期の衝突」です。スマートフォンでプレイしてそのままスリープにし、次にPCで別セッションを始めた場合、どちらのセーブデータが「正しい」のかをシステムは自動では判断できません。衝突を放置すると、片方のプレイ内容が無断で上書きされ、プレイヤーは数時間分の進行を失います。 衝突解決の設計は「実装できるかどうか」より「どの戦略を選ぶか」の問題です。戦略の選択はゲームのジャンルやデータ構造によって異なり、正解は一つではありません。この記事では、衝突が起きる仕組みから始めて、主要な解決戦略の判断基準、実装上の落とし穴の順に整理します。

この記事の目次
  1. 衝突はなぜ起きるのか
  2. 主要な衝突解決戦略と選び方
  3. 実装時に見落としやすい三つの落とし穴
  4. アソバースのSDKを使う場合の考え方
  5. 次の行動

衝突はなぜ起きるのか

クラウドセーブの基本構造は「ローカルのデータをリモートに書き込み、リモートのデータをローカルに読み込む」という単純な往復です。問題が生じるのは、オフライン中にローカルが更新され、かつリモートも別デバイスによって更新されているときです。

衝突の発生条件を整理すると次のようになります。

  • デバイスAでプレイ → オフラインのままアプリを閉じる
  • デバイスBで同じアカウントを使い別セッションを進め、クラウドに書き込む
  • デバイスAがオンラインに復帰し、ローカルのデータを書き込もうとする

この時点でリモートにはデバイスBの新しいデータがあり、デバイスAのローカルには別の新しいデータがあります。どちらも「最後に保存したタイミング」が異なるため、単純なタイムスタンプ比較では「どちらが本当に新しいか」を正しく決められないことがあります。タイムスタンプはデバイスのシステム時計に依存するため、時刻のズレや意図的な操作によって信頼性が下がるからです。

主要な衝突解決戦略と選び方

衝突解決には大きく三つの戦略があります。ゲームの性質に合わせて選ぶことが最重要です。

戦略1:Last-Write-Wins(最後書き込み優先)

最もシンプルな方法で、書き込みタイムスタンプが新しい方を正とします。実装コストは低く、シングルプレイのカジュアルゲームで衝突頻度が低い場合には十分です。

ただし、「新しい」の判断基準をサーバー側のタイムスタンプに統一することが前提条件です。デバイスの時計を使うと不整合が起きます。多くのBaaS(Backend as a Service)はデータ書き込み時にサーバー側のタイムスタンプを自動で付与するため、それを比較フィールドとして使うのが安全です。

この戦略が向かないのは、RPGのような長時間セーブや、複数デバイスを頻繁に切り替えるユーザーが多いゲームです。片方のデバイスで数時間プレイした内容がサイレントに消える可能性を、設計段階で許容できるかどうかを確認してください。

戦略2:バージョンベースの衝突検知と選択委譲

データに単調増加するバージョン番号(整数カウンター)を持たせる方法です。書き込み時に「自分が読み込んだバージョン番号」をリクエストに含め、サーバー側で現在のバージョンと一致するときだけ書き込みを受け付けます。不一致なら書き込みを拒否し、クライアントに衝突を通知します。

通知を受けたクライアントは次の二択を提示できます。

  1. リモートの新しいデータで上書きする(現在のプレイを破棄)
  2. ローカルのデータで強制書き込みする(リモートの変更を破棄)

どちらを選ぶかをプレイヤーに委ねるUIを用意する場合、「どのデバイスで・何時ごろ・どこまで進んだか」が一目でわかる情報を表示することが重要です。プレイ時間や到達ステージ名など、プレイヤーが判断できる要素を含めてください。バージョン番号だけを表示しても意味がありません。

実装のポイントは、バージョン番号をセーブデータの中ではなく、メタデータとして分離して管理することです。セーブ本体と同じオブジェクトに混ぜると、データのマージや比較が複雑になります。

戦略3:イベントログによるマージ

衝突した二つのデータを「最終状態」ではなく「操作の記録(イベントログ)」として保持しておき、後でマージする方法です。たとえばインベントリシステムであれば、「アイテムを取得」「アイテムを消費」というイベントを時系列で並べて、衝突した二つのログを統合します。

この戦略はデータの損失が最も少ない反面、マージロジックの設計コストが高いです。イベントの順序が変わると結果が変わるケース(同じアイテムを二つのデバイスで同時に消費したなど)には、別途ルールが必要です。本格的なRPGやストラテジーで実装する価値がありますが、プロトタイプ段階で選ぶ戦略ではありません。

実装時に見落としやすい三つの落とし穴

タイムスタンプの多重管理

「書き込み時刻」「最終プレイ時刻」「データ生成時刻」を別々のフィールドで管理し始めると、どれが衝突判定の根拠なのかが曖昧になります。衝突判定に使うフィールドを一つに決め、それ以外は参考情報と明示するだけで混乱が大幅に減ります。

オフライン書き込みキューの扱い

ネットワーク不通時にローカルで行ったセーブを「キュー」として積んでおき、復帰後に順次送信する実装は、一見丁寧に見えて衝突を増やします。キューに複数件が溜まっている場合、最後の一件だけを送信し、それ以前は破棄するか、あるいはすべてをまとめた「最新状態」に圧縮してから送信するのが現実的です。ゲームのセーブデータは差分ではなくスナップショットで持つ設計の方が、この問題を避けやすくなります。

衝突UIの省略

「衝突を検知したが、UIを作るのが面倒なのでサイレントに上書きした」という実装は、長期的にプレイヤーの信頼を損ないます。最低限、「別のデバイスの新しいデータが見つかりました。どちらを使いますか?」という一画面を用意することを検討してください。この画面を後から追加するのは、セーブ構造が固まった後ほど難しくなります。

アソバースのSDKを使う場合の考え方

アソバースのクラウドセーブは、作者がSDKを組み込んだゲームでのみ利用できます。SDKの仕様(書き込みAPI・バージョン管理の有無・衝突検知の方法)は公式ドキュメントで最新の内容を確認してください。仕様はアップデートで変わることがあるため、記事執筆時点の記述より公式ドキュメントを優先してください。

SDK側が衝突検知の仕組みを提供している場合でも、プレイヤーに提示するUIと判断ロジックはゲーム側で実装するのが原則です。「SDKが自動で解決してくれる」と期待して検知後の処理を省くと、未解決の衝突がローカルに残り続けるケースがあります。

次の行動

まず自分のゲームが「Last-Write-Wins」で許容できるか、それとも衝突検知とプレイヤー選択が必要かを判断してください。判断できたら、セーブデータのスキーマにバージョン番号フィールドを追加する作業から始めると、後続の実装がスムーズになります。アソバースSDKを使う場合は公式ドキュメントのクラウドセーブAPIの節を参照し、提供されている衝突検知の仕組みと自前の実装をどこで分担するかを確認してください。