Android ゲームにおける保存済みゲームのサポート

このガイドでは、Google Play Games サービスが提供するスナップショット API を使用して保存済みゲームを実装する方法について説明します。API は com.google.android.gms.games.snapshot パッケージと com.google.android.gms.games パッケージに含まれています。

始める前に

セーブ ゲームに関するコンセプトをまだ確認されていない場合は、確認することをおすすめします。

スナップショット クライアントを取得する

スナップショット API の使用を開始するには、まず SnapshotsClient オブジェクトを取得する必要があります。そのためには、Games.getSnapshotsClient() メソッドを呼び出して、アクティビティと現在のプレーヤーの GoogleSignInAccount を渡します。プレーヤーのアカウント情報を取得する方法については、Android ゲームにログインする方法についての説明をご覧ください。

ドライブ スコープを指定する

スナップショット API は、保存済みゲームの保存に Google Drive API を利用します。Drive API にアクセスするには、Google ログイン クライアントをビルドするときに、アプリで Drive.SCOPE_APPFOLDER スコープを指定する必要があります。

ログイン アクティビティの onResume() メソッドでこれを行う方法の例を次に示します。

private GoogleSignInClient mGoogleSignInClient;

@Override
protected void onResume() {
  super.onResume();
  signInSilently();
}

private void signInSilently() {
  GoogleSignInOptions signInOption =
      new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN)
          // Add the APPFOLDER scope for Snapshot support.
          .requestScopes(Drive.SCOPE_APPFOLDER)
          .build();

  GoogleSignInClient signInClient = GoogleSignIn.getClient(this, signInOption);
  signInClient.silentSignIn().addOnCompleteListener(this,
      new OnCompleteListener<GoogleSignInAccount>() {
        @Override
        public void onComplete(@NonNull Task<GoogleSignInAccount> task) {
          if (task.isSuccessful()) {
            onConnected(task.getResult());
          } else {
            // Player will need to sign-in explicitly using via UI
          }
        }
      });
}

保存済みゲームを表示する

ゲームの進行状況を保存または復元するオプションをプレーヤーに提供する場合は、スナップショット API を統合できます。ゲームはこうしたオプションを指定された保存 / 復元ポイントで表示でき、プレーヤーはいつでも進行状況を保存または復元できます。

プレーヤーがゲーム内で保存 / 復元オプションを選択すると、ゲームは必要に応じて、新しい保存済みゲームの情報を入力する画面や、既存の保存済みゲームを選択して復元する画面を表示できます。

開発を簡素化するために、スナップショット API にはデフォルトの保存済みゲーム選択ユーザー インターフェース(UI)が用意されており、すぐに使用できます。保存済みゲーム選択 UI により、プレーヤーは、新しい保存済みゲームの作成、既存の保存済みゲームの詳細表示、以前の保存済みゲームの読み込みを行えます。

デフォルトの保存済みゲーム UI を起動するには:

  1. SnapshotsClient.getSelectSnapshotIntent() を呼び出して、デフォルトの保存済みゲーム選択 UI を起動するための Intent を取得します。
  2. startActivityForResult() を呼び出して、その Intent を渡します。呼び出しが成功すると、指定したオプションとともに保存済みゲーム選択 UI がゲームに表示されます。

デフォルトの保存済みゲーム選択 UI を起動する方法の例を次に示します。

private static final int RC_SAVED_GAMES = 9009;

private void showSavedGamesUI() {
  SnapshotsClient snapshotsClient =
      Games.getSnapshotsClient(this, GoogleSignIn.getLastSignedInAccount(this));
  int maxNumberOfSavedGamesToShow = 5;

  Task<Intent> intentTask = snapshotsClient.getSelectSnapshotIntent(
      "See My Saves", true, true, maxNumberOfSavedGamesToShow);

  intentTask.addOnSuccessListener(new OnSuccessListener<Intent>() {
    @Override
    public void onSuccess(Intent intent) {
      startActivityForResult(intent, RC_SAVED_GAMES);
    }
  });
}

プレーヤーが、新しい保存済みゲームを作成するか既存の保存済みゲームを読み込むかを選択すると、UI は Google Play Games サービスにリクエストを送信します。リクエストが成功すると、Google Play Games サービスは onActivityResult() コールバックを通じて、保存済みゲームを作成または復元するための情報を返します。ゲームは、このコールバックをオーバーライドして、リクエスト中にエラーが発生したかどうかを確認できます。

次のコード スニペットは、onActivityResult() の実装サンプルを示しています。

private String mCurrentSaveName = "snapshotTemp";

/**
 * This callback will be triggered after you call startActivityForResult from the
 * showSavedGamesUI method.
 */
@Override
protected void onActivityResult(int requestCode, int resultCode,
                                Intent intent) {
  if (intent != null) {
    if (intent.hasExtra(SnapshotsClient.EXTRA_SNAPSHOT_METADATA)) {
      // Load a snapshot.
      SnapshotMetadata snapshotMetadata =
          intent.getParcelableExtra(SnapshotsClient.EXTRA_SNAPSHOT_METADATA);
      mCurrentSaveName = snapshotMetadata.getUniqueName();

      // Load the game data from the Snapshot
      // ...
    } else if (intent.hasExtra(SnapshotsClient.EXTRA_SNAPSHOT_NEW)) {
      // Create a new snapshot named with a unique string
      String unique = new BigInteger(281, new Random()).toString(13);
      mCurrentSaveName = "snapshotTemp-" + unique;

      // Create the new snapshot
      // ...
    }
  }
}

保存済みゲームを作成する

保存済みゲームにコンテンツを保存するには:

  1. SnapshotsClient.open() を介してスナップショットを非同期的に開きます。次に、SnapshotsClient.DataOrConflict.getData() を呼び出して、タスクの結果から Snapshot オブジェクトを取得します。
  2. SnapshotsClient.SnapshotConflict を介して SnapshotContents インスタンスを取得します。
  3. SnapshotContents.writeBytes() を呼び出して、プレーヤーのデータをバイト形式で保存します。
  4. すべての変更が書き込まれたら、SnapshotsClient.commitAndClose() を呼び出して、Google のサーバーに変更を送信します。このメソッド呼び出しで、ゲームは必要に応じて追加情報を提供し、この保存済みゲームをプレーヤーに提示する方法を Google Play Games Services に伝えることができます。この情報は、ゲームが SnapshotMetadataChange.Builder を使用して作成する SnapshotMetaDataChange オブジェクトで表されます。

次のスニペットは、保存済みゲームに対する変更を commit する方法を示しています。

private Task<SnapshotMetadata> writeSnapshot(Snapshot snapshot,
                                             byte[] data, Bitmap coverImage, String desc) {

  // Set the data payload for the snapshot
  snapshot.getSnapshotContents().writeBytes(data);

  // Create the change operation
  SnapshotMetadataChange metadataChange = new SnapshotMetadataChange.Builder()
      .setCoverImage(coverImage)
      .setDescription(desc)
      .build();

  SnapshotsClient snapshotsClient =
      Games.getSnapshotsClient(this, GoogleSignIn.getLastSignedInAccount(this));

  // Commit the operation
  return snapshotsClient.commitAndClose(snapshot, metadataChange);
}

アプリが SnapshotsClient.commitAndClose() を呼び出した際にプレーヤーのデバイスがネットワークに接続されていない場合、Google Play Games サービスは、保存済みゲームのデータをデバイス上にローカルに保存します。デバイスが再接続されると、Google Play Games サービスは、ローカルにキャッシュされた保存済みゲームの変更を Google のサーバーに同期します。

保存済みゲームを読み込む

現在ログインしているプレーヤーの保存済みゲームを取得するには:

  1. SnapshotsClient.open() を介してスナップショットを非同期的に開きます。次に、SnapshotsClient.DataOrConflict.getData() を呼び出して、タスクの結果から Snapshot オブジェクトを取得します。また、保存済みゲームを表示するで説明したように、保存済みゲーム選択 UI で特定のスナップショットを取得することもできます。
  2. SnapshotsClient.SnapshotConflict を使用して SnapshotContents インスタンスを取得します。
  3. SnapshotContents.readFully() を呼び出して、スナップショットのコンテンツを読み取ります。

次のスニペットは、特定の保存済みゲームを読み込む方法を示しています。

Task<byte[]> loadSnapshot() {
  // Display a progress dialog
  // ...

  // Get the SnapshotsClient from the signed in account.
  SnapshotsClient snapshotsClient =
      Games.getSnapshotsClient(this, GoogleSignIn.getLastSignedInAccount(this));

  // In the case of a conflict, the most recently modified version of this snapshot will be used.
  int conflictResolutionPolicy = SnapshotsClient.RESOLUTION_POLICY_MOST_RECENTLY_MODIFIED;

  // Open the saved game using its name.
  return snapshotsClient.open(mCurrentSaveName, true, conflictResolutionPolicy)
      .addOnFailureListener(new OnFailureListener() {
        @Override
        public void onFailure(@NonNull Exception e) {
          Log.e(TAG, "Error while opening Snapshot.", e);
        }
      }).continueWith(new Continuation<SnapshotsClient.DataOrConflict<Snapshot>, byte[]>() {
        @Override
        public byte[] then(@NonNull Task<SnapshotsClient.DataOrConflict<Snapshot>> task) throws Exception {
          Snapshot snapshot = task.getResult().getData();

          // Opening the snapshot was a success and any conflicts have been resolved.
          try {
            // Extract the raw data from the snapshot.
            return snapshot.getSnapshotContents().readFully();
          } catch (IOException e) {
            Log.e(TAG, "Error while reading Snapshot.", e);
          }

          return null;
        }
      }).addOnCompleteListener(new OnCompleteListener<byte[]>() {
        @Override
        public void onComplete(@NonNull Task<byte[]> task) {
          // Dismiss progress dialog and reflect the changes in the UI when complete.
          // ...
        }
      });
}

保存済みゲームの競合を処理する

ゲームでスナップショット API を使用する場合、複数のデバイスが同じ保存済みゲームに対して読み書きを行う可能性があります。デバイスが一時的にネットワーク接続を失ってから再接続するとデータ競合が発生することがあります。この場合、プレーヤーのローカル デバイスに保存されている保存済みゲームが、Google のサーバーに保存されているリモート バージョンと同期していません。

スナップショット API は、競合する保存済みゲームのセットを読み取り時に提示し、ゲームに適した解決戦略を実装できる競合解決メカニズムを提供します。

Google Play Games サービスでデータ競合が検出されると、SnapshotsClient.DataOrConflict.isConflict() メソッドは true 値を返します。この場合 SnapshotsClient.SnapshotConflict クラスは、保存済みゲームの 2 つのバージョンを提供します。

  • サーバー バージョン: Google Play Games サービスで認識されている、プレーヤーのデバイスに適した最新バージョン。
  • ローカル バージョン: 競合するコンテンツまたはメタデータを含むプレーヤーのデバイスのいずれかで検出された修正バージョン。これは、保存しようとしたバージョンとは異なることがあります。

ゲームは、提供されたバージョンのいずれかを選択するか、2 つの保存済みゲーム バージョンのデータを統合することで、競合の解決方法を決定する必要があります。

保存済みゲームの競合を検出して解決するには:

  1. SnapshotsClient.open() を呼び出します。タスクの結果に SnapshotsClient.DataOrConflict クラスが含まれます。
  2. SnapshotsClient.DataOrConflict.isConflict() メソッドを呼び出します。結果が true の場合、解決すべき競合があります。
  3. SnapshotsClient.DataOrConflict.getConflict() を呼び出して、SnaphotsClient.snapshotConflict インスタンスを取得します。
  4. SnapshotsClient.SnapshotConflict.getConflictId() を呼び出して、検出された競合を一意に識別する競合 ID を取得します。この値は、後で競合解決リクエストを送信する際に必要となります。
  5. SnapshotsClient.SnapshotConflict.getConflictingSnapshot() を呼び出して、ローカル バージョンを取得します。
  6. SnapshotsClient.SnapshotConflict.getSnapshot() を呼び出して、サーバー バージョンを取得します。
  7. 保存済みゲームの競合を解決するには、サーバーに保存するバージョンを最終バージョンとして選択し、SnapshotsClient.resolveConflict() メソッドに渡します。

次のスニペットは、最近変更された保存済みゲームを、保存する最終バージョンとして選択することで、保存済みゲームの競合を処理する方法の例を示しています。

private static final int MAX_SNAPSHOT_RESOLVE_RETRIES = 10;

Task<Snapshot> processSnapshotOpenResult(SnapshotsClient.DataOrConflict<Snapshot> result,
                                         final int retryCount) {

  if (!result.isConflict()) {
    // There was no conflict, so return the result of the source.
    TaskCompletionSource<Snapshot> source = new TaskCompletionSource<>();
    source.setResult(result.getData());
    return source.getTask();
  }

  // There was a conflict.  Try resolving it by selecting the newest of the conflicting snapshots.
  // This is the same as using RESOLUTION_POLICY_MOST_RECENTLY_MODIFIED as a conflict resolution
  // policy, but we are implementing it as an example of a manual resolution.
  // One option is to present a UI to the user to choose which snapshot to resolve.
  SnapshotsClient.SnapshotConflict conflict = result.getConflict();

  Snapshot snapshot = conflict.getSnapshot();
  Snapshot conflictSnapshot = conflict.getConflictingSnapshot();

  // Resolve between conflicts by selecting the newest of the conflicting snapshots.
  Snapshot resolvedSnapshot = snapshot;

  if (snapshot.getMetadata().getLastModifiedTimestamp() <
      conflictSnapshot.getMetadata().getLastModifiedTimestamp()) {
    resolvedSnapshot = conflictSnapshot;
  }

  return Games.getSnapshotsClient(theActivity, GoogleSignIn.getLastSignedInAccount(this))
      .resolveConflict(conflict.getConflictId(), resolvedSnapshot)
      .continueWithTask(
          new Continuation<
              SnapshotsClient.DataOrConflict<Snapshot>,
              Task<Snapshot>>() {
            @Override
            public Task<Snapshot> then(
                @NonNull Task<SnapshotsClient.DataOrConflict<Snapshot>> task)
                throws Exception {
              // Resolving the conflict may cause another conflict,
              // so recurse and try another resolution.
              if (retryCount < MAX_SNAPSHOT_RESOLVE_RETRIES) {
                return processSnapshotOpenResult(task.getResult(), retryCount + 1);
              } else {
                throw new Exception("Could not resolve snapshot conflicts");
              }
            }
          });
}

保存済みゲームを変更して競合を解決する

複数の保存済みゲームのデータを統合する場合や、既存の Snapshot を修正し、解決済みの最終バージョンとしてサーバーに保存する場合は、次の手順を行います。

  1. SnapshotsClient.open() を呼び出します。
  2. SnapshotsClient.SnapshotConflict.getResolutionSnapshotsContent() を呼び出して、新しい SnapshotContents オブジェクトを取得します。
  3. SnapshotsClient.SnapshotConflict.getConflictingSnapshot()SnapshotsClient.SnapshotConflict.getSnapshot() のデータを、前のステップの SnapshotContents オブジェクトに統合します。
  4. メタデータ フィールドに変更があった場合は、必要に応じて SnapshotMetadataChange インスタンスを作成します。
  5. SnapshotsClient.resolveConflict() を呼び出します。メソッド呼び出しで、SnapshotsClient.SnapshotConflict.getConflictId() を最初の引数として渡し、先ほど変更した SnapshotMetadataChange オブジェクトと SnapshotContents オブジェクトをそれぞれ 2 番目と 3 番目の引数として渡します。
  6. SnapshotsClient.resolveConflict() の呼び出しが成功すると、API は Snapshot オブジェクトをサーバーに保存し、ローカル デバイスで Snapshot オブジェクトを開こうとします。