Android 游戏中的存档游戏支持

本指南将向您展示如何使用 Google Play 游戏服务提供的快照 API 实现存档游戏。这些 API 位于 com.google.android.gms.games.snapshotcom.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. 调用 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 游戏服务发送请求。如果请求成功,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. 完成所有更改后,调用 SnapshotsClient.commitAndClose() 将更改发送到 Google 的服务器。在方法调用中,您的游戏可以选择提供其他信息来告诉 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 提供了一种冲突解决机制,该机制在读取时呈现两组冲突的保存的游戏,并允许您实现适合您游戏的解决策略。

当 Google Play 游戏服务检测到数据冲突时,SnapshotsClient.DataOrConflict.isConflict() 方法返回 true 的值。在这种情况下,SnapshotsClient.SnapshotConflict 类提供保存游戏的两个版本。

  • 服务器版本:Google Play 游戏服务认为对于玩家设备最准确的最新版本;以及
  • 本地版本:在玩家的某个设备上检测到的已修改版本,其中包含冲突的内容或元数据。这可能与您尝试保存的版本不同。

您的游戏必须通过选择提供的版本之一或合并两个保存的游戏版本的来决定如何解决冲突。

检测和解决保存的游戏冲突

  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() 作为第一个参数传递,并将您之前修改的 SnapshotMetadataChangeSnapshotContents 对象分别作为第二个和第三个参数传递。
  6. 如果 SnapshotsClient.resolveConflict() 调用成功,则 API 会将 Snapshot 对象存储到服务器,并尝试在您的本地设备上打开 Snapshot 对象。