Androidゲームでのセーブドゲームのサポート



  • この記事は、Google Playゲームサービスに関する記事を和訳したものです。
  • 原文: Saved Games Support in Android Games
  • 元記事のライセンスは CC-BYで、この和訳記事のライセンスは CC-BYです。
  • 自己責任でご利用ください。
  • 和訳した時期は 2019年7月ころです。

このガイドでは、Google Playゲームサービスによって提供されるスナップショット APIを使用して、セーブドゲームのゲームを実装する方法を示します。 APIは、com.google.android.gms.games.snapshotおよび com.google.android.gms.gamesパッケージにあります。

始める前に

もしまだそうしていなければ、セーブドゲームのゲームコンセプトをレビューすることは役立つかもしれません。

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

スナップショット APIを使用し始めるには、あなたのゲームは、まず SnapshotsClientオブジェクトを取得しなければなりません。 Games.getSnapshotsClient()メソッドを呼び出し、アクティビティと、現在のプレイヤーの GoogleSignInAccountを渡すことによって、これを行うことができます。 プレイヤーのアカウント情報を取得する方法については、Androidゲームでのサインインを参照してください。

Driveスコープを指定する

スナップショット 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. デフォルトのセーブドゲーム選択 UIを起動するための Intentを取得するために、SnapshotsClient.getSelectSnapshotIntent()を呼び出します。
  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ゲームサービスにリクエストを送信します。 もしリクエストが成功したならば、Google Playゲームサービスは、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. ひとたびすべてのあなたの変更が書き込まれたならば、あなたの変更を Googleのサーバに送信するために SnapshotsClient.commitAndClose()を呼び出します。 メソッド呼び出しにて、あなたのゲームは、必要に応じて、このセーブドゲームをプレイヤーに提示する方法を Google Playゲームサービスに伝えるための追加的な情報を提供することができます。 この情報は SnapshotMetaDataChangeオブジェクトで表されます、それは、あなたのゲームが SnapshotMetadataChange.Builderを使用して作成します。

次のスニペットは、あなたのゲームがセーブドゲームに変更をコミットする方法を示しています:

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ゲームサービスはセーブドゲームのデータを端末上にローカルに格納します。 端末の再接続時に、Google Playゲームサービスはローカルにキャッシュされたセーブドゲームの変更を 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は、読み取り時に競合したセーブドゲームの 2つのセットを提示する競合解決メカニズムを提供し、あなたのゲームに適した解決戦略を実装させます。

Google Playゲームサービスがデータの競合を検出したとき、SnapshotsClient.DataOrConflict.isConflict()メソッドは、trueの値を返します。 この場合、SnapshotsClient.SnapshotConflictクラスは、セーブドゲームの 2つのバージョンを提供します:

  • サーババージョン: プレイヤーの端末にとって正確である Google Playゲームサービスによって知られている最新のバージョン; および
  • ローカルバージョン: プレイヤーの端末のいずれかで検出された、競合するコンテンツあるいはメタデータを含んだ修正されたバージョン。 これは保存しようとしたバージョンと同じではないかもしれません。

あなたのゲームは、提供されたバージョンのいずれかを選択する、あるいは、2つのセーブドゲームのバージョンのデータをマージすることによって、競合を解決する方法を決定しなければなりません。

セーブドゲームの競合を検出し、解決するには:

  1. SnapshotsClient.open()を呼び出します。 タスクの結果は、SnapshotsClient.DataOrConflictクラスを含んでいます。
  2. SnapshotsClient.DataOrConflict.isConflict()メソッドを呼び出します。 もし結果が真であるならば、解決するための競合を持っています。
  3. SnaphotsClient.snapshotConflictインスタンスを取得するために SnapshotsClient.DataOrConflict.getConflict() を呼び出します。
  4. 検出された競合を一意に識別する競合 IDを取得するために、SnapshotsClient.SnapshotConflict.getConflictId()を呼び出します。 あなたのゲームは、後で競合解決のリクエストを送信するためにこの値を必要とします。
  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. 新しい SnapshotContentsオブジェクトを取得するために、SnapshotsClient.SnapshotConflict.getResolutionSnapshotsContent()を呼び出します。
  3. 前の手順からの SnapshotContentsオブジェクトに、SnapshotsClient.SnapshotConflict.getConflictingSnapshot()および SnapshotsClient.SnapshotConflict.getSnapshot()からのデータをマージします。
  4. 必要に応じて、メタデータのフィールドに変更がある場合には、SnapshotMetadataChangeを作成します。
  5. SnapshotsClient.resolveConflict()を呼び出します。 あなたのメソッド呼び出しにて、最初の引数として SnapshotsClient.SnapshotConflict.getConflictId()を、2番目および 3番目の引数として、先に修正した SnapshotMetadataChangeおよび SnapshotContentsを、それぞれ渡します。
  6. もし SnapshotsClient.resolveConflict()呼び出しが成功したならば、APIは、サーバに Snapshotオブジェクトを格納し、あなたのローカル端末上でスナップショットオブジェクトを開こうとします。